// Scroll view for Mac OS X
// Copyright 2006 (C) Ralph Thomas

#include <view/scroll.h>
#include <view/rectangle.h>
#include <adobe/eve.hpp>
#include <adobe/eve_evaluate.hpp>
#include <adobe/eve_parser.hpp>
#include "carbon_safe.hpp"

static const int kOffset = 5;
static const int kOffsetX2 = kOffset * 2;

namespace view { class ScrollLayout; }

//
/// This namespace contains functions related to the scrollpanel
/// which can be scrolled inside an HIScrollView.
//
namespace widget {
		OSStatus HICreateScrollPanel( HIViewRef* out, view::ScrollLayout* layout );
}

namespace view {
	//
	/// The ScrollLayout class is a subclass of Eve that owns a scrollview
	/// control.
	//
	class ScrollLayout : public adobe::eve_t {
		HIViewRef		_scroll;	///< Scroll view containing our children.
		HIViewRef		_panel;		///< Panel to scroll around in.
		adobe::sheet_t&	_sheet;		///< Sheet containing virtual box (perhaps).
		adobe::name_t	_vbox;		///< Name of virtual box in sheet.
		rectangle		_last;		///< Previous virtual box (for debouncing).
		bool			_gravity;	///< Is gravity mode on?
		HIRect			_bounds;	///< Our current bounds.
		bool			_placing;	///< Do we need to place or invalidate?
	  public:
		ScrollLayout( adobe::sheet_t& sheet, const adobe::name_t& vbox,
			bool gravity )
		  : _scroll( 0 ), _panel( 0 ), _sheet( sheet ), _vbox( vbox ),
		    _gravity( gravity ), _placing( false )
		{
			//
			// First create our scrollview widget.
			//
			HIScrollViewCreate( kHIScrollViewOptionsVertScroll |
				kHIScrollViewOptionsHorizScroll, &_scroll );
			//
			// Scrollbar autohide currently does not work. The scrollview
			// decides to show the scrollbar *after* the children have
			// been adjusted. Adjusting them again isn't very efficient or
			// fast.
			//
			HIScrollViewSetScrollBarAutoHide( _scroll, true );
			//
			// The scrollview doesn't seem to be able to deal with having
			// multiple children to scroll. Therefore we put a user pane
			// inside the scrollview and set it to be large enough to contain
			// all of it's children when we evaluate.
			//
			widget::HICreateScrollPanel( &_panel, this );
			HIViewAddSubview( _scroll, _panel ); 
			HIViewSetVisible( _panel, true );
			HIViewSetVisible( _scroll, true );
			//
			// Set our initial zero bounds.
			//
			_bounds.origin.x = _bounds.origin.y = 0;
			_bounds.size.width = _bounds.size.height = 0;
		}
		virtual ~ScrollLayout() {
			DisposeControl( _scroll );
			DisposeControl( _panel );
		}
		HIViewRef getControl() const { return _scroll; }
		HIViewRef getPanel() const { return _panel; }
		void place( const adobe::point_2d_t& pos, const adobe::extents_t& geo ) {
			_bounds.origin.x = pos.x_m;
			_bounds.origin.y = pos.y_m;
			_bounds.size.width = geo.width();
			_bounds.size.height = geo.height();
			

			_placing = true;
			//
			// Trigger Eve to evaluate what it can do with the new space.
			//
			evaluate( evaluate_nested );
			_placing = false;
		}
		virtual void evaluate( adobe::eve_t::evaluate_options_t options ) {
			//
			// We really need our internal bounds, but those might not
			// be available. (So perhaps we have to remove the scrollbar
			// size ourselves...).
			//
			adobe::eve_t::evaluate( options );
			
			if( _placing ) {
				HIViewSetFrame( _scroll, &_bounds );
			} else {
				EventRef theEvent;
				CreateEvent( 0, kEventClassScrollable,
					kEventScrollableInfoChanged, GetCurrentEventTime(),
					kEventAttributeUserEvent, &theEvent );
				if( theEvent ) {
					SendEventToEventTarget( theEvent,
						GetControlEventTarget( _scroll ) );
					ReleaseEvent( theEvent );
				}
			}
		}
		
	  
	};

	adobe::widget_node_t scroll_factory(
		const adobe::dictionary_t&		parameters,
		const adobe::widget_node_t&		parent,
		const adobe::factory_token_t&	token,
		factory* f )
	{
		//
		// See if we can get the name to push the bounding box into.
		//
		adobe::name_t bboxName;
		parameters.get( adobe::static_name_t( "virtual_box" ), bboxName );
		//
		// Do we want gravity for this scrollview?
		//
		bool gravity( false );
		parameters.get( adobe::static_name_t( "gravity" ), gravity );
		//
		// Create the scrollview widget.
		//
		ScrollLayout* sl( new ScrollLayout( token.sheet_m, bboxName, gravity ) );
		HIViewRef control( sl->getControl() );
		HIViewRef panel( sl->getPanel() );
		//
		// Insert this widget into it's parent on the main display.
		//
		adobe::display_t::position_t display_token( adobe::insert(
			adobe::get_main_display(), parent.display_token_m, control ) );
		adobe::display_t::position_t panel_display_token( adobe::insert(
			adobe::get_main_display(), display_token, panel ) );
		adobe::display_t::position_t overlay_token(
			token.overlay_m.insert( parent.overlay_token_m ) );
		//
		// Create factory token for children.
		//
		factory_token_ptr_t child_token;
		child_token.layout_m = sl;
		//
		// We can register our new token with the factory. That means that
		// any child widgets will get our token.
		//
		f->push( factory::factory_t( child_token, 0 ) );
		//
		// Bind the calculate and place signals for Eve.
		//
		eve_client::place_func_t placer( boost::bind( &ScrollLayout::place,
			sl, _1, _2 ) );
		//
		// Attempt to get a minimum width and height from the parameters
		// but default to 160 otherwise.
		//
		int width = 160; int height = 160;
		parameters.get<int>( adobe::static_name_t( "min_width" ),
			width );
		parameters.get<int>( adobe::static_name_t( "min_height" ),
			height );

		adobe::extents_t extents;
		extents.width() = width; extents.height() = height;
		adobe::eve_t::signal_suite_t signals;
		signals.eve_calculate_proc_m = boost::bind(
			&eve_client::utility_widget_calculate,
			extents, _1 );
		signals.eve_place_proc_m = boost::bind(
			&eve_client::utility_widget_place, placer, _1, _2,
			boost::ref( token.overlay_m ), overlay_token );
		//
		// Register the scrollgroup with the parent's Eve instance.
		//
		adobe::eve_t::iterator eve_token(
			token.layout_m.add_view_element( parent.eve_token_m,
				adobe::eve_t::insert_element_t(
					adobe::eve_t::calculate_data_t(),
					false,
					parameters,
					signals )
				)
			);
		//
		// Insert a stub entry into our own Eve layout engine so that children
		// get placed with a horizontal and vertical offset (to make them look
		// okay).
		//
		adobe::eve_t::calculate_data_t cdata;
		adobe::set_margin( cdata, kOffset );
		cdata.create_m = false;
		cdata.placement_m = adobe::eve_t::place_row;
		cdata.spacing_m[1] = kOffset;
		
		adobe::eve_t::iterator child_eve_token(
			sl->add_view_element( adobe::eve_t::iterator(),
				adobe::eve_t::insert_element_t(
					cdata,
					true,
					adobe::dictionary_t(),
					adobe::eve_t::signal_suite_t() )
				)
			);
		//
		// Delete the things we created when the assemblage gets destroyed.
		//
		token.assemblage_m.delete_on_destruction( sl );
		//
		// Return the scroll group.
		// 
		return adobe::widget_node_t( parent.size_m, child_eve_token,
			panel_display_token, overlay_token );
	}
}

namespace widget {
	pascal OSStatus ScrollPanelHandler( EventHandlerCallRef inCaller,
		EventRef inEvent, void* inData ) {
		OSStatus	result		= eventNotHandledErr;
		view::ScrollLayout* layout	= (view::ScrollLayout*)inData;
		HIViewRef	userPane	= layout->getPanel();
		HIRect		bounds;
		//
		// This event handler code is based on the Apple Custom HIView
		// Tutorial.
		//
		HIViewGetBounds( userPane, &bounds );
		if( GetEventClass( inEvent ) == kEventClassControl ) {
			if( GetEventKind( inEvent ) != kEventControlDraw ) return result;
			//
			// We need to draw ourselves all white.
			//
			CGContextRef context = 0;
			GetEventParameter(inEvent, kEventParamCGContextRef,
				typeCGContextRef, NULL, sizeof( context ), NULL, &context );
			if( !context ) return result;
			//
			// Push the state, set up and draw then pop the state. This
			// is a really quick and dirty drawing routine.
			//
			CGContextSaveGState( context );
			CGContextSetGrayFillColor( context, 1.0, 1.0 );
			CGContextSetLineWidth( context, 1.0 );
			CGContextSetGrayStrokeColor( context, 0.0, 1.0 );
			CGRect cgBounds = bounds;
			CGContextFillRect( context, bounds );
			CGContextStrokeRect( context, bounds );
			CGContextRestoreGState( context );
			return noErr;
		}
		switch( GetEventKind( inEvent ) ) {
			case kEventScrollableGetInfo: {
				HISize lineSize = {1, 1};
				HISize imageSize = {3000, 2000};
				HIRect frame;
				//
				// Adjust all of our children.
				//
				if( bounds.size.width > 0.0f && bounds.size.height > 0.0f )
					layout->adjust( (int)bounds.size.width,
						(int)bounds.size.height,
						adobe::eve_t::evaluate_nested );
				//
				// Determine our maximum size from our children.
				//
				HIViewRef first = HIViewGetFirstSubview( userPane );
				float xmax = 0.0f; float ymax = 0.0f;
				int counted = 0;
				int total = 0;
				while( first ) {
					if( HIViewIsVisible( first ) ) {
						HIViewGetFrame( first, &frame );
						if( frame.origin.x + frame.size.width > xmax )
							xmax = frame.origin.x + frame.size.width;
						if( frame.origin.y + frame.size.height > ymax )
							ymax = frame.origin.y + frame.size.height;
						counted++;
					}
					total++;
					first = HIViewGetNextView( first );
				}
				imageSize.width = xmax + kOffsetX2;
				imageSize.height = ymax + kOffsetX2;
				//
				// Return information to the scroll view about how much
				// of us it can scroll.
				//
				SetEventParameter( inEvent, kEventParamViewSize, typeHISize,
					sizeof( bounds.size ), &bounds.size );
				SetEventParameter( inEvent, kEventParamImageSize, typeHISize,
					sizeof( imageSize ), &imageSize );
				SetEventParameter( inEvent, kEventParamLineSize, typeHISize,
					sizeof( lineSize ), &lineSize );
				SetEventParameter( inEvent, kEventParamOrigin, typeHIPoint,
					sizeof( bounds.origin ), &bounds.origin );
				result = noErr;
				break;
			}
			case kEventScrollableScrollTo: {
				HIPoint where;
				//
				// We need to scroll to the location given in the event. We
				// only do anything if the target point is different to the
				// current point.
				//
				GetEventParameter(inEvent, kEventParamOrigin, typeHIPoint,
					NULL, sizeof( where ), NULL, &where );
				//
				// Because we didn't put a real widget in to place with the
				// offset we have to manually subtract it here.
				//
				where.x -= kOffset;
				where.y -= kOffset;
				if( (bounds.origin.x != where.x) || (bounds.origin.y != where.y) ) {
					HIViewSetBoundsOrigin( userPane, where.x, where.y );
					HIViewSetNeedsDisplay( userPane, true );
				}
				result = noErr;
				break;
			}
		}
		return result;
	}
	OSStatus HICreateScrollPanel( HIViewRef* out, view::ScrollLayout* layout ) {
		OSStatus 	status = noErr;
		Rect		bounds = { 0, 0, 10, 10 };
		*out = 0;
		//
		// We create a userpane control and then install our custom event
		// handler in it for scrolling.
		//
		status = CreateUserPaneControl( NULL, &bounds,
			kControlSupportsEmbedding, out );
		if( status != noErr ) return status;
		//
		// Install our custom event handler into the user pane control.
		//
		EventTypeSpec userPaneEvents[] = {
			{ kEventClassScrollable, kEventScrollableGetInfo },
			{ kEventClassScrollable, kEventScrollableScrollTo },
			{ kEventClassControl, kEventControlDraw }
		};
		InstallControlEventHandler( *out, ScrollPanelHandler, 3,
			userPaneEvents, layout, NULL );
		return status;
	}
}